SuperStrict

Rem
bbdoc: Input sequence module
about: This module provides a simple yet powerful input sequence acceptance interface.
With this module, it is easy to add cheat codes and easter-eggs to your own game! This module
can also be used where you need to perform actions on certain input sequences, such as
"power moves" in fighting games.

Each input sequence is thought to be made up of any number of "strokes". A stroke is simply an
input from the keyboard, mouse or joystick. A single stroke can consist of multiple simultaneous key/button
presses - this kind of stroke is referred to as a "macro-stroke".
      
End Rem
Module jmd.inputsequence

ModuleInfo "Version: 1.0"
ModuleInfo "Author: Jonathan Decker"
ModuleInfo "License: GNU GPL v3"
ModuleInfo "SVN: http://jmd-blitzmax-modules.googlecode.com/svn/trunk/jmd.mod/inputsequence.mod"

ModuleInfo "History: 1.0"
ModuleInfo "History:	Initial release."


Import brl.linkedlist
Import brl.polledinput
Import pub.freejoy
Import brl.blitz
Import brl.standardio
Import brl.map

'Create a map of alphanumeric characters to KeyCodes
Private
Global keyCodes:TMap = CreateMap()
'alpha
keyCodes.insert("a",String(KEY_A))
keyCodes.insert("b",String(KEY_B))
keyCodes.insert("c",String(KEY_C))
keyCodes.insert("d",String(KEY_D))
keyCodes.insert("e",String(KEY_E))
keyCodes.insert("f",String(KEY_F))
keycodes.insert("g",String(KEY_G))
keyCodes.insert("h",String(KEY_H))
keyCodes.insert("i",String(KEY_I))
keyCodes.insert("j",String(KEY_J))
keyCodes.insert("k",String(KEY_K))
keyCodes.insert("l",String(KEY_L))
keyCodes.insert("m",String(KEY_M))
keyCodes.insert("n",String(KEY_N))
keyCodes.insert("o",String(KEY_O))
keyCodes.insert("p",String(KEY_P))
keyCodes.insert("q",String(KEY_Q))
keyCodes.insert("r",String(KEY_R))
keyCodes.insert("s",String(KEY_S))
keyCodes.insert("t",String(KEY_T))
keyCodes.insert("u",String(KEY_U))
keyCodes.insert("v",String(KEY_V))
keyCodes.insert("w",String(KEY_W))
keyCodes.insert("x",String(KEY_X))
keyCodes.insert("y",String(KEY_Y))
keyCodes.insert("z",String(KEY_Z))
'numeric
keyCodes.insert("0",String(KEY_0))
keyCodes.insert("1",String(KEY_1))
keyCodes.insert("2",String(KEY_2))
keyCodes.insert("3",String(KEY_3))
keyCodes.insert("4",String(KEY_4))
keyCodes.insert("5",String(KEY_5))
keyCodes.insert("6",String(KEY_6))
keyCodes.insert("7",String(KEY_7))
keyCodes.insert("8",String(KEY_8))
keyCodes.insert("9",String(KEY_9))
'misc
keyCodes.insert(" ",String(KEY_SPACE))
keyCodes.insert(".",String(KEY_PERIOD))
keyCodes.insert("~t",String(KEY_TAB))
keyCodes.insert("-",String(KEY_MINUS))
Public

Rem
bbdoc: @strokeType constant
about: Used as a parameter to the #InSeqAddStroke() function
End Rem
Const KEY_STROKE:Int = 1
Rem
bbdoc: @strokeType constant
about: Used as a parameter to the #InSeqAddStroke() function
End Rem
Const JOY_STROKE:Int = 2
Rem
bbdoc: @strokeType constant
about: Used as a parameter to the #InSeqAddStroke() function
End Rem
Const MOUSE_STROKE:Int = 4


Rem
bbdoc: TInputSequence type
about: This is the type used and accessed by your program.
End Rem
Type TInputSequence
	Global InSeq:TList
	Field numStrokes:Int
	Field strokeCounter:Int
	Field strokes:TList
	Field timeOut:Int
	Field lastStrokeTimestamp:Int
	Field timesEntered:Int
	
	Rem
	bbdoc: alternative for #CreateInputSequence()
	End Rem
	Function Create:TInputSequence(thisTimeOut:Int=500)
		Local a:TInputSequence = New TInputSequence
		a.numStrokes=0
		a.strokeCounter=0
		a.timesEntered=0
		a.timeOut=thisTimeOut
		a.strokes=CreateList()
		If Not a.InSeq Then a.InSeq=CreateList()
		a.InSeq.AddLast(a)
		Return a
	End Function
	
	Rem
	bbdoc: alternative for #InSeqAddStroke()
	End Rem
	Method AddStroke(strokeType:Int,strokeValue:Int,strokeNumber:Int=0)
		'Filter the strokeNumber!
		'This is neccessary to prevent problems if it is given out of order!
		If strokeNumber<>0
			If strokeNumber<>numStrokes And strokeNumber<>numStrokes+1
				RuntimeError("TInputSequence.AddStroke strokeNumber error!~n"..
				+ "The strokeNumber must be the same as, or one higher than the previous strokeNumber!")
			EndIf
		EndIf
		

		'See if we are in auto-sequence mode (strokeNumber is set to 0)
		If strokeNumber=0
			Self.numStrokes:+ 1
			strokeNumber=numStrokes
		Else
			If strokeNumber>Self.numStrokes Then Self.numStrokes:+ 1
		EndIf
		
		
		Local a:Tstroke = New Tstroke
		a.strokeType=strokeType
		a.strokeValue=strokeValue
		a.strokeNum=strokeNumber
		strokes.addLast(a)
	End Method
	
	Rem
	bbdoc: alternative for #InSeqAddKeyStrokesFromString()
	End Rem
	Method AddKeyStrokesFromString(thisString:String)
		For Local i:Int = 0 To thisString.length-1
			AddStroke(KEY_STROKE,Int(String(MapValueForKey(keyCodes,Chr(thisString[i])))))
		Next
	End Method
	
	Rem
	bbdoc: alternative For #InSeqSuccessful()
	End Rem
	Method Successful:Int()
		If (Self.strokeCounter=Self.numStrokes) 
			Self.strokeCounter=0
			Self.timesEntered:+ 1
			Return True
		Else
			Return False
		EndIf
	End Method
	
	Rem
	bbdoc: alternative For #InSeqEntered()
	End Rem
	Method Entered:Int()
		Return Self.timesEntered
	End Method
	
	Rem
	bbdoc: alternative For #InSeqResetEntered()
	End Rem
	Method ResetEntered()
		Self.timesEntered=0
	End Method
	
	Rem
	bbdoc: alternative For #InSeqClear()
	End Rem
	Method Clear()
		Self.strokes=CreateList()
	End Method
	

	Rem
	bbdoc: alternative for #InSeqUpdateAll()
	End Rem
	Function UpdateAll()
		
		'GET THE INPUTS
		Global allKeyStrokes:Int[]
		allKeyStrokes=TInputSequence.GetKeys()

		Local allJoyStrokes:Int[]
		allJoyStrokes=TInputSequence.GetJoyButtons()
		Local allMouseStrokes:Int[]
		allMouseStrokes=TInputSequence.GetMouseButtons()
		
		For Local thisIS:TInputSequence = EachIn InSeq
			If MilliSecs()-thisIS.lastStrokeTimestamp > thisIS.timeOut And thisIS.strokeCounter<>0
				'We're back to the first stroke!
				thisIS.strokeCounter=0
			EndIf

					
			'See if we have all of the strokes we should have
			If (allKeystrokes.length>0) And (allKeyStrokes.length <> GetNumStrokes(thisIS,KEY_STROKE,thisIS.strokeCounter+1))
				thisIS.strokeCounter=0
				Continue
			EndIf
			If (allJoyStrokes.length>0) And (allJoyStrokes.length <> GetNumStrokes(thisIS,JOY_STROKE,thisIS.strokeCounter+1))
				thisIS.strokeCounter=0
				Continue
			EndIf
			If (allMouseStrokes.length>0) And (allMouseStrokes.length <> GetNumStrokes(thisIS,MOUSE_STROKE,thisIS.StrokeCounter+1)) 
				thisIS.strokeCounter=0
				Continue
			EndIf

			'Now, lets process the strokes!	
			Local verified:Int=True
			For Local thisStroke:Tstroke = EachIn thisIS.strokes
				If thisStroke.strokeNum = thisIS.strokeCounter+1
					
					Select thisStroke.strokeType
					Case KEY_STROKE
						If Not TInputSequence.ContainsStroke(allKeyStrokes,thisStroke.strokeValue)
							verified=False
						EndIf
					Case JOY_STROKE
						If Not TInputSequence.ContainsStroke(allJoyStrokes,thisStroke.strokeValue) 
							verified=False
						EndIf
					Case MOUSE_STROKE
						If Not TInputSequence.ContainsStroke(allMouseStrokes,thisStroke.strokeValue)
							verified=False
						EndIf
					End Select
				EndIf
			Next

			If verified
				thisIS.lastStrokeTimestamp=MilliSecs()
				thisIS.strokeCounter:+ 1
			Else
				If allKeyStrokes.length Or allJoyStrokes.length Or allMouseStrokes.length
					thisIS.strokeCounter=0
				EndIf
			EndIf
		
		
		Next
	EndFunction
	
	'The following functions are private and for internal use!
	Function GetNumStrokes:Int(thisInputSequence:TInputSequence,thisStrokeType:Int,strokeNumber:Int)
		Local retVal:Int=0
		
		For Local thisStroke:Tstroke = EachIn thisInputSequence.strokes
			If thisStroke.strokeNum = strokeNumber And thisStroke.strokeType & thisStrokeType Then retVal:+ 1
		Next
		Return retVal
	End Function
	
	Function ContainsStroke:Int(thisArray:Int[],thisStrokeValue:Int)
		For Local i:Int = 0 To  thisArray.length-1
			If thisArray[i]=thisStrokeValue 
				Return True
			EndIf
		Next
		Return False
	End Function
	
	Function GetKeys:Int[]()
		Global lastState:Byte[256]
		Local j:Int =0
		Local keys:Int[j]
		
		For Local i:Int = 0 To 255
			If KeyDown(i) 
				If lastState[i]=False
					lastState[i]=True
					keys=keys[0..j+1]
					keys[j]=i
					j:+ 1
				EndIf
			Else
				lastState[i]=False
			EndIf
		Next
		Return keys
	End Function
	
	Function GetJoyButtons:Int[]()
		Global lastState:Byte[16]
		Local j:Int=0
		Local buttons:Int[j]
		
		For Local i:Int = 0 To 15
			If JoyDown(i)
				If lastState[i]=False
					lastState[i]=True
					buttons=buttons[0..j+1]
					buttons[j]=i
					j:+ 1
				EndIf
			Else
				lastState[i]=False
			EndIf
		Next
		Return buttons
	End Function

	Function GetMouseButtons:Int[]()
		Global lastState:Byte[3]
		Local j:Int=0
		Local buttons:Int[j]
		
		For Local i:Int = 0 To 2
			If MouseDown(i+1)
				If lastState[i]=False
					lastState[i]=True
					buttons=buttons[0..j+1]
					buttons[j]=i+1
					j:+ 1
				EndIf
			Else
				lastState[i]=False
			EndIf
		Next
		Return buttons
	End Function
End Type

Rem
bbdoc: TStroke type
about: This type is used internally, and is not normally used from outside of this module.
End Rem
Type Tstroke
	Field strokeType:Int
	Field strokeValue:Int
	Field strokeNum:Int
End Type

Rem
bbdoc: Create a new Input Sequence
about: Create a new nput Sequence. The @timeOut parameter specifies the amount of time
in milliseconds that is allowed between each stroke of the Input Sequence. Smaller values
will require the user to have to enter the Input Sequence faster. while larger values will
allow for more time for the Input Sequence to be entered, making it easier to enter successfully.
End Rem
Function CreateInputSequence:TInputSequence(timeOut:Int=500)
	Return TInputSequence.Create(timeOut)
End Function

Rem
bbdoc: Add to the Input Sequence
about: Add a "stroke" to the Input Sequence. At this time, each stroke can be a
keyboard key, or a joystick button.
the @strokeType parameter expects either @KEY_STROKE or @JOY_STROKE. The @strokeValue parameter
will either be a keyboard code (I.e. @KEY_SPACE) or a joystick button number, depending on which value 
was used for @strokeType
End Rem
Function InSeqAddStroke(thisInputSequence:TInputSequence,strokeType:Int,strokeValue:Int,strokeNumber:Int=0)
	thisInputSequence.AddStroke(strokeType,strokeValue,strokeNumber)
End Function

Rem
bbdoc: Batch add keystrokes from a string
about: This is a convenience function to easily add many @{KEYSTROKE}s at once. This is useful for text-based cheat codes
such as having the user type "heaven" for god-mode, or "ammo-up" to add ammo, for example.
End Rem
Function InSeqAddKeyStrokesFromString(thisInputSequence:TInputSequence,thisString:String)
	thisInputSequence.AddKeyStrokesFromString(thisString)
End Function

Rem
bbdoc: Check If an Input Sequence was successfully entered
returns: @True if anInput Sequence was successfully entered, false otherwise
about: Us this to check if an Input Sequence was entered successfully.
End Rem
Function InSeqSuccessful:Int(thisInputSequence:TInputSequence)
	Return thisInputSequence.Successful()
End Function

Rem
bbdoc: Track Input Sequence successes
returns: The number of times an Input Sequencet has been entered successfully.
about: This is a convenience function for tracking how many times an Input Sequence has been entered. This is useful if you wish to limit
the number of times a cheat code can be performed by the user for example - such as only allowing an "add ammo" cheat to be used once per game.
example:
&{
if InSeqSuccessful(ammoCheat)
	if InSeqEntered(ammoCheat)>1
		print "Oops!  You can only use that cheat once!"
	else
		player.ammo:+ 1000 'add the ammo!
	endif
endif
}

see also: InSeqResetEntered()
End Rem
Function InSeqEntered:Int(thisInputSequence:TInputSequence)
	Return thisInputSequence.Entered()
End Function

Rem
bbdoc: Reset the Input Sequence tracking
about: Use this function to reset the Input Sequence tracking back to 0
End Rem
Function InSeqResetEntered(thisInputSequence:TInputSequence)
	thisInputSequence.ResetEntered()
End Function

Rem
bbdoc: Clear all strokes from Input Sequence
about: Use this function to clear/remove all strokes from the Input Sequence.  This could be used, for example,
if the player changed control configuration and you need to rebuild the Input Sequence using the new strokes.
End Rem
Function InSeqClear(thisInputSequence:TInputSequence)
	thisInputSequence.Clear()
End Function

Rem
bbdoc: Update all InputSequences
about: This function must be called every iteration of your loop. It is responsible for
updating the state of all of your TInputSequence instances!
End Rem
Function InSeqUpdateAll()
	TInputSequence.UpdateAll()
End Function

Rem
bbdoc: Release the TInputSequence instance
about: Free the TInputSequence input so that the garbage collector can clean up.
note: You must manually free the instance, or the CG will not clean up and it will create a small memory leak.
This is because the TInputSequence type stores a global list of instances, so merely setting your instance to null will not
trip the GC to do its job.
End Rem
Function FreeInputSequence(thisInputSequence:TInputSequence)
	ListRemove(thisInputSequence.InSeq,thisInputSequence)
	thisInputSequence=Null
End Function